/* * Copyright (C) 2015 Fastboot Mobile, LLC. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, see <http://www.gnu.org/licenses>. */ package com.fastbootmobile.encore.app.ui; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.Log; import android.view.animation.AccelerateDecelerateInterpolator; import com.fastbootmobile.encore.art.RecyclingBitmapDrawable; /** * <p> * Class that allows drawable transitions in a way that fits Google's Material Design specifications * (see <a href="http://www.google.com/design/spec/patterns/imagery-treatment.html">Material Design * pattern 'Imagery Treatment'</a>). * </p> */ public class MaterialTransitionDrawable extends Drawable { private static final String TAG = "MaterialTransDrawable"; public static final long DEFAULT_DURATION = 1000; public static final long SHORT_DURATION = 300; private BitmapDrawable mBaseDrawable; private RecyclingBitmapDrawable mTargetDrawable; private BitmapDrawable mOfflineDrawable; private final AccelerateDecelerateInterpolator mInterpolator; private long mStartTime; private boolean mAnimating; private long mDuration = DEFAULT_DURATION; private ColorMatrix mColorMatSaturation; private Paint mPaint; private boolean mShowOfflineOverdraw; private long mOfflineStartTime; private ColorFilter mExtColorFilter; private final Object mDrawLock = new Object(); public MaterialTransitionDrawable(BitmapDrawable offlineDrawable, BitmapDrawable base) { this(offlineDrawable); mBaseDrawable = base; } public MaterialTransitionDrawable(BitmapDrawable offlineDrawable) { mInterpolator = new AccelerateDecelerateInterpolator(); mAnimating = false; mShowOfflineOverdraw = false; mColorMatSaturation = new ColorMatrix(); mPaint = new Paint(); mOfflineDrawable = offlineDrawable; } public BitmapDrawable getFinalDrawable() { synchronized (mDrawLock) { if (mTargetDrawable != null) { return mTargetDrawable; } else { return mBaseDrawable; } } } public void setTransitionDuration(long durationMillis) { synchronized (mDrawLock) { mDuration = durationMillis; } } public void setImmediateTo(BitmapDrawable drawable) { synchronized (mDrawLock) { // Cancel animation mAnimating = false; mTargetDrawable = null; mShowOfflineOverdraw = false; // Set new drawable as base and draw it if (mBaseDrawable != null && mBaseDrawable instanceof RecyclingBitmapDrawable) { ((RecyclingBitmapDrawable) mBaseDrawable).setIsDisplayed(false); } mBaseDrawable = drawable; mBaseDrawable.setBounds(getBounds()); if (mBaseDrawable instanceof RecyclingBitmapDrawable) { ((RecyclingBitmapDrawable) mBaseDrawable).setIsDisplayed(true); } } invalidateSelf(); } public void transitionTo(final RecyclingBitmapDrawable drawable) { synchronized (mDrawLock) { if (drawable != mTargetDrawable) { mTargetDrawable = drawable; mTargetDrawable.setBounds(getBounds()); mStartTime = -1; mAnimating = true; } } } public void setShowOfflineOverdraw(boolean show) { if (mShowOfflineOverdraw != show) { synchronized (mDrawLock) { mShowOfflineOverdraw = show; mOfflineStartTime = SystemClock.uptimeMillis(); } invalidateSelf(); } } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); synchronized (mDrawLock) { if (mBaseDrawable != null & !mAnimating) { mBaseDrawable.setBounds(bounds); } if (mTargetDrawable != null) { mTargetDrawable.setBounds(bounds); } } } @Override public void draw(Canvas canvas) { synchronized (mDrawLock) { if (mAnimating) { if (mStartTime < 0) { mStartTime = SystemClock.uptimeMillis(); } final float rawProgress = Math.min(1.0f, ((float) (SystemClock.uptimeMillis() - mStartTime)) / ((float) mDuration)); // As per the Material Design spec, animation goes into 3 steps. Ranging from 0 to 100, // opacity is full at 50, exposure (gamma + black output) at 75, and saturation at 100. // For performance, we only do the saturation and opacity transition final float inputOpacity = Math.min(1.0f, rawProgress * (1.0f / 0.5f)); // final float inputExposure = Math.min(1.0f, rawProgress * (1.0f / 0.75f)); final float progressOpacity = mInterpolator.getInterpolation(inputOpacity); // final float progressExposure = 1.0f - mInterpolator.getInterpolation(inputExposure); final float progressSaturation = mInterpolator.getInterpolation(rawProgress); if (mBaseDrawable != null) { drawTranslatedBase(canvas); } if (mExtColorFilter == null) { mColorMatSaturation.setSaturation(progressSaturation); ColorMatrixColorFilter colorMatFilter = new ColorMatrixColorFilter(mColorMatSaturation); mPaint.setAlpha((int) (progressOpacity * 255.0f)); mPaint.setColorFilter(colorMatFilter); } else { mPaint.setColorFilter(mExtColorFilter); } if (!mTargetDrawable.getBitmap().isRecycled()) { try { canvas.drawBitmap(mTargetDrawable.getBitmap(), 0, 0, mPaint); } catch (RuntimeException e) { Log.w(TAG, "Couldn't write target bitmap!"); } } if (rawProgress >= 1.0f) { mAnimating = false; if (mBaseDrawable != null && mBaseDrawable instanceof RecyclingBitmapDrawable) { ((RecyclingBitmapDrawable) mBaseDrawable).setIsDisplayed(false); } mBaseDrawable = mTargetDrawable; } else { invalidateSelf(); } } else if (mBaseDrawable != null) { if (!mBaseDrawable.getBitmap().isRecycled()) { try { mBaseDrawable.draw(canvas); } catch (Exception e) { Log.w(TAG, "Couldn't draw base drawable: " + e.getMessage()); } } } if (mShowOfflineOverdraw) { int alpha = (int) Math.min(160, (SystemClock.uptimeMillis() - mOfflineStartTime) / 4); canvas.drawColor(0x00888888 | ((alpha & 0xFF) << 24)); mPaint.setAlpha(alpha * 255 / 160); canvas.drawBitmap(mOfflineDrawable.getBitmap(), getBounds().centerX() - mOfflineDrawable.getIntrinsicWidth() / 2, getBounds().centerY() - mOfflineDrawable.getIntrinsicHeight() / 2, mPaint); if (alpha != 160) { invalidateSelf(); } } } } private void drawTranslatedBase(Canvas canvas) { // Pad the base drawable to be at the center of the target size final float targetWidth = mTargetDrawable.getIntrinsicWidth(); final float targetHeight = mTargetDrawable.getIntrinsicHeight(); Rect baseBounds = mBaseDrawable.getBounds(); final float baseWidth = baseBounds.width(); final float baseHeight = baseBounds.height(); canvas.save(); float scaling; if (targetWidth > targetHeight) { scaling = Math.max(targetWidth / baseWidth, targetHeight / baseHeight); } else { scaling = Math.min(targetWidth / baseWidth, targetHeight / baseHeight); } final float scaledBaseWidth = scaling * baseWidth; final float scaledBaseHeight = scaling * baseHeight; canvas.translate((targetWidth - scaledBaseWidth) * 0.5f, (targetHeight - scaledBaseHeight) * 0.5f); canvas.scale(scaling, scaling); if (!mBaseDrawable.getBitmap().isRecycled()) { mBaseDrawable.draw(canvas); } canvas.restore(); } @Override public int getIntrinsicHeight() { if (mAnimating && mTargetDrawable != null) { return mTargetDrawable.getIntrinsicHeight(); } else if (mBaseDrawable != null) { return mBaseDrawable.getIntrinsicHeight(); } else { return super.getIntrinsicHeight(); } } @Override public int getIntrinsicWidth() { if (mAnimating && mTargetDrawable != null) { return mTargetDrawable.getIntrinsicWidth(); } else if (mBaseDrawable != null) { return mBaseDrawable.getIntrinsicWidth(); } else { return super.getIntrinsicWidth(); } } @Override public void setAlpha(int i) { } @Override public void setColorFilter(ColorFilter colorFilter) { mExtColorFilter = colorFilter; invalidateSelf(); } @Override public int getOpacity() { return 255; } }